﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using NetworkCommsDotNet;
using System.Windows;

using System.Windows.Forms;
using System.ComponentModel;
using System.IO;
using ProtoBuf;
using DPSBase;
using System.Collections.ObjectModel;

namespace BACKOFFICE
{
    
    class Networking
    {
        
        #region Private Fields
        /// <summary>
        /// Datacontext for the GUI list box
        /// </summary>
        public static ObservableCollection<ReceivedFile> receivedFiles = new ObservableCollection<ReceivedFile>();

        /// <summary>
        /// References to recieved files by remote ConnectionInfo
        /// </summary>
        public static Dictionary<ConnectionInfo, Dictionary<string, ReceivedFile>> receivedFilesDict = new Dictionary<ConnectionInfo, Dictionary<string, ReceivedFile>>();

        /// <summary>
        /// Incoming partial data cache. Keys are ConnectionInfo, PacketSequenceNumber. Value is partial packet data.
        /// </summary>
        public static Dictionary<ConnectionInfo, Dictionary<long, byte[]>> incomingDataCache = new Dictionary<ConnectionInfo, Dictionary<long, byte[]>>();

        /// <summary>
        /// Incoming sendInfo cache. Keys are ConnectionInfo, PacketSequenceNumber. Value is sendInfo.
        /// </summary>
        public static Dictionary<ConnectionInfo, Dictionary<long, SendInfo>> incomingDataInfoCache = new Dictionary<ConnectionInfo, Dictionary<long, SendInfo>>();

        /// <summary>
        /// Custom sendReceiveOptions used for sending files. Can be changed via GUI.
        /// </summary>
        public static SendReceiveOptions customOptions = new SendReceiveOptions<ProtobufSerializer>();

        /// <summary>
        /// Object used for ensuring thread safety.
        /// </summary>
        public static object syncRoot = new object();

        /// <summary>
        /// Boolean used for surpressing errors during GUI close
        /// </summary>
        public static volatile bool windowClosing = false;
        #endregion

        public static void StartListening()
        {
            //Trigger IncomingPartialFileData method if we receive a packet of type 'PartialFileData'
            NetworkComms.AppendGlobalIncomingPacketHandler<byte[]>("PartialFileData", IncomingPartialFileData);
            //Trigger IncomingPartialFileDataInfo method if we receive a packet of type 'PartialFileDataInfo'
            NetworkComms.AppendGlobalIncomingPacketHandler<SendInfo>("PartialFileDataInfo", IncomingPartialFileDataInfo);

            //Trigger the method OnConnectionClose so that we can do some cleanup
            NetworkComms.AppendGlobalConnectionCloseHandler(OnConnectionClose);

            //Start listening for TCP connections
            TCPConnection.StartListening(true);

            //Write out some usefull debugging information the log window
            //AddLineToLog("Initialised WPF file transfer example. Accepting TCP connections on:");
            foreach (var listenEndPoint in TCPConnection.ExistingLocalListenEndPoints())
                MessageBox.Show(listenEndPoint.Address + ":" + listenEndPoint.Port);
            
        }

        public static void IncomingPartialFileData(PacketHeader header, Connection connection, byte[] data)
        {
            try
            {
                SendInfo info = null;
                ReceivedFile file = null;

                //Perform this in a thread safe way
                lock (syncRoot)
                {
                    //Extract the packet sequence number from the header
                    //The header can also user defined parameters
                    long sequenceNumber = header.GetOption(PacketHeaderLongItems.PacketSequenceNumber);

                    if (incomingDataInfoCache.ContainsKey(connection.ConnectionInfo) && incomingDataInfoCache[connection.ConnectionInfo].ContainsKey(sequenceNumber))
                    {
                        //We have the associated SendInfo so we can add this data directly to the file
                        info = incomingDataInfoCache[connection.ConnectionInfo][sequenceNumber];
                        incomingDataInfoCache[connection.ConnectionInfo].Remove(sequenceNumber);

                        //Check to see if we have already recieved any files from this location
                        if (!receivedFilesDict.ContainsKey(connection.ConnectionInfo))
                            receivedFilesDict.Add(connection.ConnectionInfo, new Dictionary<string, ReceivedFile>());

                        //Check to see if we have already initialised this file
                        if (!receivedFilesDict[connection.ConnectionInfo].ContainsKey(info.Filename))
                        {
                            receivedFilesDict[connection.ConnectionInfo].Add(info.Filename, new ReceivedFile(info.Filename, connection.ConnectionInfo, info.TotalBytes));
                            //AddNewReceivedItem(receivedFilesDict[connection.ConnectionInfo][info.Filename]);
                        }

                        file = receivedFilesDict[connection.ConnectionInfo][info.Filename];
                    }
                    else
                    {
                        //We do not yet have the associated SendInfo so we just add the data to the cache
                        if (!incomingDataCache.ContainsKey(connection.ConnectionInfo))
                            incomingDataCache.Add(connection.ConnectionInfo, new Dictionary<long, byte[]>());

                        incomingDataCache[connection.ConnectionInfo].Add(sequenceNumber, data);
                    }
                }

                //If we have everything we need we can add data to the ReceivedFile
                if (info != null && file != null && !file.IsCompleted)
                {
                    file.AddData(info.BytesStart, 0, data.Length, data);

                    //Perform a little cleanup
                    file = null;
                    data = null;
                    GC.Collect();
                }
                else if (info == null ^ file == null)
                    throw new Exception("Either both are null or both are set. Info is " + (info == null ? "null." : "set.") + " File is " + (file == null ? "null." : "set.") + " File is " + (file.IsCompleted ? "completed." : "not completed."));
            }
            catch (Exception ex)
            {
                //If an exception occurs we write to the log window and also create an error file
                //AddLineToLog("Exception - " + ex.ToString());
                NetworkComms.LogError(ex, "IncomingPartialFileDataError");
            }

            
        }

        public static void IncomingPartialFileDataInfo(PacketHeader header, Connection connection, SendInfo info)
        {
            try
            {
                byte[] data = null;
                ReceivedFile file = null;

                //Perform this in a thread safe way
                lock (syncRoot)
                {
                    //Extract the packet sequence number from the header
                    //The header can also user defined parameters
                    long sequenceNumber = info.PacketSequenceNumber;

                    if (incomingDataCache.ContainsKey(connection.ConnectionInfo) && incomingDataCache[connection.ConnectionInfo].ContainsKey(sequenceNumber))
                    {
                        //We already have the associated data in the cache
                        data = incomingDataCache[connection.ConnectionInfo][sequenceNumber];
                        incomingDataCache[connection.ConnectionInfo].Remove(sequenceNumber);

                        //Check to see if we have already recieved any files from this location
                        if (!receivedFilesDict.ContainsKey(connection.ConnectionInfo))
                            receivedFilesDict.Add(connection.ConnectionInfo, new Dictionary<string, ReceivedFile>());

                        //Check to see if we have already initialised this file
                        if (!receivedFilesDict[connection.ConnectionInfo].ContainsKey(info.Filename))
                        {
                            receivedFilesDict[connection.ConnectionInfo].Add(info.Filename, new ReceivedFile(info.Filename, connection.ConnectionInfo, info.TotalBytes));
                            //AddNewReceivedItem(receivedFilesDict[connection.ConnectionInfo][info.Filename]);
                        }

                        file = receivedFilesDict[connection.ConnectionInfo][info.Filename];
                    }
                    else
                    {
                        //We do not yet have the necessary data corresponding with this SendInfo so we add the
                        //info to the cache
                        if (!incomingDataInfoCache.ContainsKey(connection.ConnectionInfo))
                            incomingDataInfoCache.Add(connection.ConnectionInfo, new Dictionary<long, SendInfo>());

                        incomingDataInfoCache[connection.ConnectionInfo].Add(sequenceNumber, info);
                    }
                }

                //If we have everything we need we can add data to the ReceivedFile
                if (data != null && file != null && !file.IsCompleted)
                {
                    file.AddData(info.BytesStart, 0, data.Length, data);

                    //Perform a little cleanup
                    file = null;
                    data = null;
                    GC.Collect();
                }
                else if (data == null ^ file == null)
                    throw new Exception("Either both are null or both are set. Data is " + (data == null ? "null." : "set.") + " File is " + (file == null ? "null." : "set.") + " File is " + (file.IsCompleted ? "completed." : "not completed."));
            }
            catch (Exception ex)
            {
                //If an exception occurs we write to the log window and also create an error file
                //AddLineToLog("Exception - " + ex.ToString());
                NetworkComms.LogError(ex, "IncomingPartialFileDataInfo");
            }
        }

        public static void OnConnectionClose(Connection conn)
        {
            ReceivedFile[] filesToRemove = null;

            lock (syncRoot)
            {
                //Remove any associated data from the caches
                incomingDataCache.Remove(conn.ConnectionInfo);
                incomingDataInfoCache.Remove(conn.ConnectionInfo);

                //Remove any non completed files
                if (receivedFilesDict.ContainsKey(conn.ConnectionInfo))
                {
                    filesToRemove = (from current in receivedFilesDict[conn.ConnectionInfo] where !current.Value.IsCompleted select current.Value).ToArray();
                    receivedFilesDict[conn.ConnectionInfo] = (from current in receivedFilesDict[conn.ConnectionInfo] where current.Value.IsCompleted select current).ToDictionary(entry => entry.Key, entry => entry.Value);
                }
            }

            //Update the GUI
            //lbReceivedFiles.Dispatcher.BeginInvoke(new Action(() =>
            //{
            //    lock (syncRoot)
            //    {
            //        if (filesToRemove != null)
            //        {
            //            foreach (ReceivedFile file in filesToRemove)
            //            {
            //                receivedFiles.Remove(file);
            //                file.Close();
            //            }
            //        }
            //    }
            //}));

            //Write some usefull information the log window
            //AddLineToLog("Connection closed with " + conn.ConnectionInfo.ToString());
        }

        public static void SendFileToServer(string file,string RemoteIP,string Port)
        {
                    //Perform the send in a task so that we don't lock the GUI
            Task.Factory.StartNew(() =>
            {
                try
                {
                    //Create a fileStream from the selected file
                    FileStream stream = new FileStream(file, FileMode.Open, FileAccess.Read);

                    //Wrap the fileStream in a threadSafeStream so that future operations are thread safe
                    ThreadSafeStream safeStream = new ThreadSafeStream(stream);

                    //Get the filename without the associated path information
                    string shortFileName = System.IO.Path.GetFileName(file);

                    //Parse the remote connectionInfo
                    //We have this in a seperate try catch so that we can write a clear message to the log window
                    //if there are problems
                    ConnectionInfo remoteInfo;
                    try
                    {
                        remoteInfo = new ConnectionInfo(RemoteIP, int.Parse(Port));
                    }
                    catch (Exception)
                    {
                        throw new InvalidDataException("Failed to parse remote IP and port. Check and try again.");
                    }

                    //Get a connection to the remote side
                    Connection connection = TCPConnection.GetConnection(remoteInfo);

                    //Break the send into 20 segments. The less segments the less overhead 
                    //but we still want the progress bar to update in sensible steps
                    long sendChunkSizeBytes = (long)(stream.Length / 20.0) + 1;

                    //Limit send chunk size to 500MB
                    long maxChunkSizeBytes = 500L * 1024L * 1024L;
                    if (sendChunkSizeBytes > maxChunkSizeBytes) sendChunkSizeBytes = maxChunkSizeBytes;

                    long totalBytesSent = 0;
                    do
                    {
                        //Check the number of bytes to send as the last one may be smaller
                        long bytesToSend = (totalBytesSent + sendChunkSizeBytes < stream.Length ? sendChunkSizeBytes : stream.Length - totalBytesSent);

                        //Wrap the threadSafeStream in a StreamSendWrapper so that we can get NetworkComms.Net
                        //to only send part of the stream.
                        StreamSendWrapper streamWrapper = new StreamSendWrapper(safeStream, totalBytesSent, bytesToSend);

                        //We want to record the packetSequenceNumber
                        long packetSequenceNumber;
                        //Send the select data
                        connection.SendObject("PartialFileData", streamWrapper, customOptions, out packetSequenceNumber);
                        //Send the associated SendInfo for this send so that the remote can correctly rebuild the data
                        connection.SendObject("PartialFileDataInfo", new SendInfo(shortFileName, stream.Length, totalBytesSent, packetSequenceNumber), customOptions);

                        totalBytesSent += bytesToSend;

                    } while (totalBytesSent < stream.Length);

                    //Clean up any unused memory
                    GC.Collect();


                    MessageBox.Show("Completed file send to '" + connection.ConnectionInfo.ToString() + "'.");
                }
                catch (CommunicationException)
                {
                    //If there is a communication exception then we just write a connection
                    //closed message to the log window
                    MessageBox.Show("Failed to complete send as connection was closed.");
                }
                catch (Exception ex)
                {
                    //If we get any other exception which is not an InvalidDataException
                    //we log the error
                    if (!windowClosing && ex.GetType() != typeof(InvalidDataException))
                    {
                        MessageBox.Show(ex.Message.ToString());
                        NetworkComms.LogError(ex, "SendFileError");
                    }
                }
            });
        }

        //private void SendFileButton_Click(object sender, RoutedEventArgs e)
        //{
        //    //Create an OpenFileDialog so that we can request the file to send
        //    OpenFileDialog openDialog = new OpenFileDialog();
        //    openDialog.Multiselect = false;

        //    //If a file was selected
        //    if (openDialog.ShowDialog() == true)
        //    {
        //        //Disable the send and compression buttons
        //        sendFileButton.IsEnabled = false;
        //        UseCompression.IsEnabled = false;

        //        //Parse the neccessary remote information
        //        string filename = openDialog.FileName;
        //        string remoteIP = this.remoteIP.Text;
        //        string remotePort = this.remotePort.Text;

        //        //Set the send progress bar to 0
        //        UpdateSendProgress(0);

        //        //Perform the send in a task so that we don't lock the GUI
        //        Task.Factory.StartNew(() =>
        //        {
        //            try
        //            {
        //                //Create a fileStream from the selected file
        //                FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read);

        //                //Wrap the fileStream in a threadSafeStream so that future operations are thread safe
        //                ThreadSafeStream safeStream = new ThreadSafeStream(stream);

        //                //Get the filename without the associated path information
        //                string shortFileName = System.IO.Path.GetFileName(filename);

        //                //Parse the remote connectionInfo
        //                //We have this in a seperate try catch so that we can write a clear message to the log window
        //                //if there are problems
        //                ConnectionInfo remoteInfo;
        //                try
        //                {
        //                    remoteInfo = new ConnectionInfo(remoteIP, int.Parse(remotePort));
        //                }
        //                catch (Exception)
        //                {
        //                    throw new InvalidDataException("Failed to parse remote IP and port. Check and try again.");
        //                }

        //                //Get a connection to the remote side
        //                Connection connection = TCPConnection.GetConnection(remoteInfo);

        //                //Break the send into 20 segments. The less segments the less overhead 
        //                //but we still want the progress bar to update in sensible steps
        //                long sendChunkSizeBytes = (long)(stream.Length / 20.0) + 1;

        //                //Limit send chunk size to 500MB
        //                long maxChunkSizeBytes = 500L * 1024L * 1024L;
        //                if (sendChunkSizeBytes > maxChunkSizeBytes) sendChunkSizeBytes = maxChunkSizeBytes;

        //                long totalBytesSent = 0;
        //                do
        //                {
        //                    //Check the number of bytes to send as the last one may be smaller
        //                    long bytesToSend = (totalBytesSent + sendChunkSizeBytes < stream.Length ? sendChunkSizeBytes : stream.Length - totalBytesSent);

        //                    //Wrap the threadSafeStream in a StreamSendWrapper so that we can get NetworkComms.Net
        //                    //to only send part of the stream.
        //                    StreamSendWrapper streamWrapper = new StreamSendWrapper(safeStream, totalBytesSent, bytesToSend);

        //                    //We want to record the packetSequenceNumber
        //                    long packetSequenceNumber;
        //                    //Send the select data
        //                    connection.SendObject("PartialFileData", streamWrapper, customOptions, out packetSequenceNumber);
        //                    //Send the associated SendInfo for this send so that the remote can correctly rebuild the data
        //                    connection.SendObject("PartialFileDataInfo", new SendInfo(shortFileName, stream.Length, totalBytesSent, packetSequenceNumber), customOptions);

        //                    totalBytesSent += bytesToSend;

        //                    //Update the GUI with our send progress
        //                    UpdateSendProgress((double)totalBytesSent / stream.Length);
        //                } while (totalBytesSent < stream.Length);

        //                //Clean up any unused memory
        //                GC.Collect();

        //                AddLineToLog("Completed file send to '" + connection.ConnectionInfo.ToString() + "'.");
        //            }
        //            catch (CommunicationException)
        //            {
        //                //If there is a communication exception then we just write a connection
        //                //closed message to the log window
        //                AddLineToLog("Failed to complete send as connection was closed.");
        //            }
        //            catch (Exception ex)
        //            {
        //                //If we get any other exception which is not an InvalidDataException
        //                //we log the error
        //                if (!windowClosing && ex.GetType() != typeof(InvalidDataException))
        //                {
        //                    AddLineToLog(ex.Message.ToString());
        //                    NetworkComms.LogError(ex, "SendFileError");
        //                }
        //            }

        //            //Once the send is finished reset the send progress bar
        //            UpdateSendProgress(0);

        //            //Once complete enable the send button again
        //            sendFileButton.Dispatcher.BeginInvoke(new Action(() =>
        //            {
        //                sendFileButton.IsEnabled = true;
        //                UseCompression.IsEnabled = true;
        //            }));
        //        });
        //    }
        //}

        public class ReceivedFile : INotifyPropertyChanged
        {
            /// <summary>
            /// The name of the file
            /// </summary>
            public string Filename { get; private set; }

            /// <summary>
            /// The connectionInfo corresponding with the source
            /// </summary>
            public ConnectionInfo SourceInfo { get; private set; }

            /// <summary>
            /// The total size in bytes of the file
            /// </summary>
            public long SizeBytes { get; private set; }

            /// <summary>
            /// The total number of bytes received so far
            /// </summary>
            public long ReceivedBytes { get; private set; }

            /// <summary>
            /// Getter which returns the completion of this file, between 0 and 1
            /// </summary>
            public double CompletedPercent
            {
                get { return (double)ReceivedBytes / SizeBytes; }
                set { throw new Exception("An attempt to modify readonly value."); }
            }

            /// <summary>
            /// A formatted string of the SourceInfo
            /// </summary>
            public string SourceInfoStr
            {
                get { return "[" + SourceInfo.RemoteEndPoint.Address + ":" + SourceInfo.RemoteEndPoint.Port + "]"; }
            }

            /// <summary>
            /// Returns true if the completed percent equals 1
            /// </summary>
            public bool IsCompleted
            {
                get { return ReceivedBytes == SizeBytes; }
            }

            /// <summary>
            /// Private object used to ensure thread safety
            /// </summary>
            object SyncRoot = new object();

            /// <summary>
            /// A memorystream used to build the file
            /// </summary>
            Stream data;

            /// <summary>
            ///Event subscribed to by GUI for updates
            /// </summary>
            public event PropertyChangedEventHandler PropertyChanged;

            /// <summary>
            /// Create a new ReceivedFile
            /// </summary>
            /// <param name="filename">Filename associated with this file</param>
            /// <param name="sourceInfo">ConnectionInfo corresponding with the file source</param>
            /// <param name="sizeBytes">The total size in bytes of this file</param>
            public ReceivedFile(string filename, ConnectionInfo sourceInfo, long sizeBytes)
            {
                this.Filename = filename;
                this.SourceInfo = sourceInfo;
                this.SizeBytes = sizeBytes;

                //We create a file on disk so that we can receive large files
                data = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 8 * 1024, FileOptions.DeleteOnClose);
            }

            /// <summary>
            /// Add data to file
            /// </summary>
            /// <param name="dataStart">Where to start writing this data to the interal memoryStream</param>
            /// <param name="bufferStart">Where to start copying data from buffer</param>
            /// <param name="bufferLength">The number of bytes to copy from buffer</param>
            /// <param name="buffer">Buffer containing data to add</param>
            public void AddData(long dataStart, int bufferStart, int bufferLength, byte[] buffer)
            {
                lock (SyncRoot)
                {
                    data.Seek(dataStart, SeekOrigin.Begin);
                    data.Write(buffer, (int)bufferStart, (int)bufferLength);

                    ReceivedBytes += (int)(bufferLength - bufferStart);
                }

                NotifyPropertyChanged("CompletedPercent");
                NotifyPropertyChanged("IsCompleted");
            }

            /// <summary>
            /// Saves the completed file to the provided saveLocation
            /// </summary>
            /// <param name="saveLocation">Location to save file</param>
            public void SaveFileToDisk(string saveLocation)
            {
                if (ReceivedBytes != SizeBytes)
                    throw new Exception("Attempted to save out file before data is complete.");

                if (!File.Exists(Filename))
                    throw new Exception("The transfered file should have been created within the local application directory. Where has it gone?");

                //File.Delete(saveLocation);
                File.Copy(Filename, saveLocation);
            }

            /// <summary>
            /// Closes and releases any resources maintained by this file
            /// </summary>
            public void Close()
            {
                try
                {
                    data.Dispose();
                }
                catch (Exception) { }

                try
                {
                    data.Close();
                }
                catch (Exception) { }
            }

            /// <summary>
            /// Triggers a GUI update on a property change
            /// </summary>
            /// <param name="propertyName"></param>
            private void NotifyPropertyChanged(string propertyName = "")
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        [ProtoContract]
        public class SendInfo
        {
            /// <summary>
            /// Corresponding filename
            /// </summary>
            [ProtoMember(1)]
            public string Filename { get; private set; }

            /// <summary>
            /// The starting point for the associated data
            /// </summary>
            [ProtoMember(2)]
            public long BytesStart { get; private set; }

            /// <summary>
            /// The total number of bytes expected for the whole ReceivedFile
            /// </summary>
            [ProtoMember(3)]
            public long TotalBytes { get; private set; }

            /// <summary>
            /// The packet sequence number corresponding to the associated data
            /// </summary>
            [ProtoMember(4)]
            public long PacketSequenceNumber { get; private set; }

            /// <summary>
            /// Private constructor required for deserialisation
            /// </summary>
            private SendInfo() { }

            /// <summary>
            /// Create a new instance of SendInfo
            /// </summary>
            /// <param name="filename">Filename corresponding to data</param>
            /// <param name="totalBytes">Total bytes of the whole ReceivedFile</param>
            /// <param name="bytesStart">The starting point for the associated data</param>
            /// <param name="packetSequenceNumber">Packet sequence number corresponding to the associated data</param>
            public SendInfo(string filename, long totalBytes, long bytesStart, long packetSequenceNumber)
            {
                this.Filename = filename;
                this.TotalBytes = totalBytes;
                this.BytesStart = bytesStart;
                this.PacketSequenceNumber = packetSequenceNumber;
            }
        }

        public class server
        {
            public static string RemoteIP = "127.0.0.1";
            public static string RemotePort = "10000";

            public static void GetLocalEndpoint()
            {
                foreach (var listenEndPoint in TCPConnection.ExistingLocalListenEndPoints())
                {

                }
            }

        }

        public class client
        {

        }

    }


}
